import _ from 'lodash';
import UrlBuilder from './url_builder';
import ResponseParser from './response_parser';
import SupportedNamespaces from './supported_namespaces';
import TimegrainConverter from '../time_grain_converter';
import {
  AzureMonitorQuery,
  AzureMonitorQueryData,
  AzureDataSourceJsonData,
  AzureMonitorMetricDefinitionsResponse,
  AzureMonitorResourceGroupsResponse,
  AzureMonitorResourceResponse,
  Resource,
} from '../types';
import { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/ui';

import { TimeSeries, toDataFrame } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';

export default class AzureMonitorDatasource {
  apiVersion = '2018-01-01';
  apiPreviewVersion = '2017-12-01-preview';
  id: number;
  subscriptionId: string;
  baseUrl: string;
  resourceGroup: string;
  resourceName: string;
  url: string;
  defaultDropdownValue = 'select';
  cloudName: string;
  supportedMetricNamespaces: string[] = [];

  /** @ngInject */
  constructor(
    private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
    private backendSrv: BackendSrv,
    private templateSrv: TemplateSrv
  ) {
    this.id = instanceSettings.id;
    this.subscriptionId = instanceSettings.jsonData.subscriptionId;
    this.cloudName = instanceSettings.jsonData.cloudName || 'azuremonitor';
    this.baseUrl = `/${this.cloudName}/subscriptions`;
    this.url = instanceSettings.url;

    this.supportedMetricNamespaces = new SupportedNamespaces(this.cloudName).get();
  }

  isConfigured(): boolean {
    return !!this.subscriptionId && this.subscriptionId.length > 0;
  }

  buildQuery(
    options: DataQueryRequest<AzureMonitorQuery>,
    target: any,
    {
      resourceGroup,
      resourceName,
      metricDefinition,
      timeGrainUnit,
      timeGrain,
      metricName,
      metricNamespace,
      allowedTimeGrainsMs,
      aggregation,
      dimension,
      dimensionFilter,
      alias,
    }: AzureMonitorQueryData,
    subscriptionId?: string
  ) {
    if (timeGrainUnit && timeGrain !== 'auto') {
      timeGrain = TimegrainConverter.createISO8601Duration(timeGrain, timeGrainUnit);
    }

    const metricNamespaceParsed = this.templateSrv.replace(metricNamespace, options.scopedVars);

    return {
      refId: target.refId,
      intervalMs: options.intervalMs,
      datasourceId: this.id,
      subscription: this.templateSrv.replace(
        subscriptionId || target.subscription || this.subscriptionId,
        options.scopedVars
      ),
      queryType: 'Azure Monitor',
      type: 'timeSeriesQuery',
      raw: false,
      azureMonitor: {
        resourceGroup: this.templateSrv.replace(resourceGroup, options.scopedVars),
        resourceName: this.templateSrv.replace(resourceName, options.scopedVars),
        metricDefinition: this.templateSrv.replace(metricDefinition, options.scopedVars),
        timeGrain: this.templateSrv.replace((timeGrain || '').toString(), options.scopedVars),
        allowedTimeGrainsMs: allowedTimeGrainsMs,
        metricName: this.templateSrv.replace(metricName, options.scopedVars),
        metricNamespace:
          metricNamespaceParsed && metricNamespaceParsed !== this.defaultDropdownValue
            ? metricNamespaceParsed
            : metricDefinition,
        aggregation: this.templateSrv.replace(aggregation, options.scopedVars),
        dimension: this.templateSrv.replace(dimension, options.scopedVars),
        dimensionFilter: this.templateSrv.replace(dimensionFilter, options.scopedVars),
        alias,
        format: target.format,
      },
    };
  }

  buildSingleQuery(
    options: DataQueryRequest<AzureMonitorQuery>,
    target: any,
    {
      resourceGroup,
      resourceName,
      metricDefinition,
      timeGrainUnit,
      timeGrain,
      metricName,
      metricNamespace,
      allowedTimeGrainsMs,
      aggregation,
      dimension,
      dimensionFilter,
      alias,
    }: AzureMonitorQueryData,
    queryMode: string
  ) {
    if (timeGrainUnit && timeGrain !== 'auto') {
      timeGrain = TimegrainConverter.createISO8601Duration(timeGrain, timeGrainUnit);
    }

    const metricNamespaceParsed = this.templateSrv.replace(metricNamespace, options.scopedVars);

    return {
      refId: target.refId,
      intervalMs: options.intervalMs,
      datasourceId: this.id,
      subscription: this.templateSrv.replace(target.subscription || this.subscriptionId, options.scopedVars),
      queryType: 'Azure Monitor',
      type: 'timeSeriesQuery',
      raw: false,
      azureMonitor: {
        queryMode,
        data: {
          [queryMode]: {
            resourceGroup: this.templateSrv.replace(resourceGroup, options.scopedVars),
            resourceName: this.templateSrv.replace(resourceName, options.scopedVars),
            metricDefinition: this.templateSrv.replace(metricDefinition, options.scopedVars),
            timeGrain: this.templateSrv.replace((timeGrain || '').toString(), options.scopedVars),
            allowedTimeGrainsMs: allowedTimeGrainsMs,
            metricName: this.templateSrv.replace(metricName, options.scopedVars),
            metricNamespace:
              metricNamespaceParsed && metricNamespaceParsed !== this.defaultDropdownValue
                ? metricNamespaceParsed
                : metricDefinition,
            aggregation: this.templateSrv.replace(aggregation, options.scopedVars),
            dimension: this.templateSrv.replace(dimension, options.scopedVars),
            dimensionFilter: this.templateSrv.replace(dimensionFilter, options.scopedVars),
            alias,
            format: target.format,
          },
        },
      },
    };
  }

  async query(options: DataQueryRequest<any>): Promise<DataQueryResponseData[]> {
    const groupedQueries: any[] = await Promise.all(
      options.targets
        .filter(item => {
          const { data, queryMode } = item.azureMonitor;
          const { resourceGroup, resourceGroups, metricDefinition, metricName } = data[queryMode];

          return (
            item.hide !== true &&
            ((resourceGroup && resourceGroup !== this.defaultDropdownValue) || resourceGroups.length) &&
            metricDefinition &&
            metricDefinition !== this.defaultDropdownValue &&
            metricName &&
            metricName !== this.defaultDropdownValue
          );
        })
        .map(async target => {
          const { data, queryMode } = target.azureMonitor;

          if (queryMode === 'crossResource') {
            const { resourceGroups, metricDefinition, locations } = data[queryMode];
            const resources = await this.getResources(target.subscriptions).then(resources =>
              resources.filter(
                ({ type, group, subscriptionId, location }) =>
                  target.subscriptions.includes(subscriptionId) &&
                  resourceGroups.includes(group) &&
                  locations.includes(location) &&
                  metricDefinition === type
              )
            );
            delete data.crossResource.metricNamespace;
            return resources.map(
              ({ type: metricDefinition, group: resourceGroup, subscriptionId, name: resourceName }) =>
                this.buildQuery(
                  options,
                  target,
                  {
                    ...data[queryMode],
                    metricDefinition,
                    resourceGroup,
                    resourceName,
                  },
                  subscriptionId
                )
            );
          } else {
            return Promise.resolve(this.buildSingleQuery(options, target, data[queryMode], queryMode));
          }
        })
    );

    const queries = _.flatten(groupedQueries);

    if (!queries || queries.length === 0) {
      return Promise.resolve([]);
    }

    const { data } = await this.backendSrv.datasourceRequest({
      url: '/api/tsdb/query',
      method: 'POST',
      data: {
        from: options.range.from.valueOf().toString(),
        to: options.range.to.valueOf().toString(),
        queries,
      },
    });

    const result: DataQueryResponseData[] = [];
    if (data.results) {
      Object.values(data.results).forEach((queryRes: any) => {
        if (!queryRes.series) {
          return;
        }
        queryRes.series.forEach((series: any) => {
          const timeSerie: TimeSeries = {
            target: series.name,
            datapoints: series.points,
            refId: queryRes.refId,
            meta: queryRes.meta,
          };
          result.push(toDataFrame(timeSerie));
        });
      });
      return result;
    }

    return Promise.resolve([]);
  }

  annotationQuery(options: any) {}

  metricFindQuery(query: string) {
    const subscriptionsQuery = query.match(/^Subscriptions\(\)/i);
    if (subscriptionsQuery) {
      return this.getSubscriptions();
    }

    const resourceGroupsQuery = query.match(/^ResourceGroups\(\)/i);
    if (resourceGroupsQuery) {
      return this.getResourceGroups(this.subscriptionId);
    }

    const resourceGroupsQueryWithSub = query.match(/^ResourceGroups\(([^\)]+?)(,\s?([^,]+?))?\)/i);
    if (resourceGroupsQueryWithSub) {
      return this.getResourceGroups(this.toVariable(resourceGroupsQueryWithSub[1]));
    }

    const metricDefinitionsQuery = query.match(/^Namespaces\(([^\)]+?)(,\s?([^,]+?))?\)/i);
    if (metricDefinitionsQuery) {
      if (!metricDefinitionsQuery[3]) {
        return this.getMetricDefinitions(this.subscriptionId, this.toVariable(metricDefinitionsQuery[1]));
      }
    }

    const metricDefinitionsQueryWithSub = query.match(/^Namespaces\(([^,]+?),\s?([^,]+?)\)/i);
    if (metricDefinitionsQueryWithSub) {
      return this.getMetricDefinitions(
        this.toVariable(metricDefinitionsQueryWithSub[1]),
        this.toVariable(metricDefinitionsQueryWithSub[2])
      );
    }

    const resourceNamesQuery = query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?)\)/i);
    if (resourceNamesQuery) {
      const resourceGroup = this.toVariable(resourceNamesQuery[1]);
      const metricDefinition = this.toVariable(resourceNamesQuery[2]);
      return this.getResourceNames(this.subscriptionId, resourceGroup, metricDefinition);
    }

    const resourceNamesQueryWithSub = query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/i);
    if (resourceNamesQueryWithSub) {
      const subscription = this.toVariable(resourceNamesQueryWithSub[1]);
      const resourceGroup = this.toVariable(resourceNamesQueryWithSub[2]);
      const metricDefinition = this.toVariable(resourceNamesQueryWithSub[3]);
      return this.getResourceNames(subscription, resourceGroup, metricDefinition);
    }

    const metricNamespaceQuery = query.match(/^MetricNamespace\(([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i);
    if (metricNamespaceQuery) {
      const resourceGroup = this.toVariable(metricNamespaceQuery[1]);
      const metricDefinition = this.toVariable(metricNamespaceQuery[2]);
      const resourceName = this.toVariable(metricNamespaceQuery[3]);
      return this.getMetricNamespaces(this.subscriptionId, resourceGroup, metricDefinition, resourceName);
    }

    const metricNamespaceQueryWithSub = query.match(
      /^metricnamespace\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i
    );
    if (metricNamespaceQueryWithSub) {
      const subscription = this.toVariable(metricNamespaceQueryWithSub[1]);
      const resourceGroup = this.toVariable(metricNamespaceQueryWithSub[2]);
      const metricDefinition = this.toVariable(metricNamespaceQueryWithSub[3]);
      const resourceName = this.toVariable(metricNamespaceQueryWithSub[4]);
      console.log(metricNamespaceQueryWithSub);
      return this.getMetricNamespaces(subscription, resourceGroup, metricDefinition, resourceName);
    }

    const metricNamesQuery = query.match(/^MetricNames\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i);
    if (metricNamesQuery) {
      if (metricNamesQuery[3].indexOf(',') === -1) {
        const resourceGroup = this.toVariable(metricNamesQuery[1]);
        const metricDefinition = this.toVariable(metricNamesQuery[2]);
        const resourceName = this.toVariable(metricNamesQuery[3]);
        const metricNamespace = this.toVariable(metricNamesQuery[4]);
        return this.getMetricNames(this.subscriptionId, resourceGroup, metricDefinition, resourceName, metricNamespace);
      }
    }

    const metricNamesQueryWithSub = query.match(
      /^MetricNames\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?(.+?)\)/i
    );

    if (metricNamesQueryWithSub) {
      const subscription = this.toVariable(metricNamesQueryWithSub[1]);
      const resourceGroup = this.toVariable(metricNamesQueryWithSub[2]);
      const metricDefinition = this.toVariable(metricNamesQueryWithSub[3]);
      const resourceName = this.toVariable(metricNamesQueryWithSub[4]);
      const metricNamespace = this.toVariable(metricNamesQueryWithSub[5]);
      return this.getMetricNames(subscription, resourceGroup, metricDefinition, resourceName, metricNamespace);
    }

    return undefined;
  }

  toVariable(metric: string) {
    return this.templateSrv.replace((metric || '').trim());
  }

  getSubscriptions(route?: string) {
    const url = `/${route || this.cloudName}/subscriptions?api-version=2019-03-01`;
    return this.doRequest(url).then((result: any) => {
      return ResponseParser.parseSubscriptions(result);
    });
  }

  getResourceGroups(subscriptionId: string) {
    const url = `${this.baseUrl}/${subscriptionId}/resourceGroups?api-version=${this.apiVersion}`;
    return this.doRequest(url).then((result: AzureMonitorResourceGroupsResponse) => {
      return ResponseParser.parseResponseValues(result, 'name', 'name');
    });
  }

  getMetricDefinitions(subscriptionId: string, resourceGroup: string) {
    const url = `${this.baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${
      this.apiVersion
    }`;
    return this.doRequest(url)
      .then((result: AzureMonitorMetricDefinitionsResponse) => {
        return ResponseParser.parseResponseValues(result, 'type', 'type');
      })
      .then((result: any) => {
        return _.filter(result, t => {
          for (let i = 0; i < this.supportedMetricNamespaces.length; i++) {
            if (t.value.toLowerCase() === this.supportedMetricNamespaces[i].toLowerCase()) {
              return true;
            }
          }

          return false;
        });
      })
      .then((result: any) => {
        let shouldHardcodeBlobStorage = false;
        for (let i = 0; i < result.length; i++) {
          if (result[i].value === 'Microsoft.Storage/storageAccounts') {
            shouldHardcodeBlobStorage = true;
            break;
          }
        }

        if (shouldHardcodeBlobStorage) {
          result.push({
            text: 'Microsoft.Storage/storageAccounts/blobServices',
            value: 'Microsoft.Storage/storageAccounts/blobServices',
          });
          result.push({
            text: 'Microsoft.Storage/storageAccounts/fileServices',
            value: 'Microsoft.Storage/storageAccounts/fileServices',
          });
          result.push({
            text: 'Microsoft.Storage/storageAccounts/tableServices',
            value: 'Microsoft.Storage/storageAccounts/tableServices',
          });
          result.push({
            text: 'Microsoft.Storage/storageAccounts/queueServices',
            value: 'Microsoft.Storage/storageAccounts/queueServices',
          });
        }

        return result;
      });
  }

  getResourceNames(subscriptionId: string, resourceGroup: string, metricDefinition: string) {
    const url = `${this.baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${
      this.apiVersion
    }`;

    return this.doRequest(url).then((result: any) => {
      if (!_.startsWith(metricDefinition, 'Microsoft.Storage/storageAccounts/')) {
        return ResponseParser.parseResourceNames(result, metricDefinition);
      }

      const list = ResponseParser.parseResourceNames(result, 'Microsoft.Storage/storageAccounts');
      for (let i = 0; i < list.length; i++) {
        list[i].text += '/default';
        list[i].value += '/default';
      }

      return list;
    });
  }

  getMetricNamespaces(subscriptionId: string, resourceGroup: string, metricDefinition: string, resourceName: string) {
    const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
      this.baseUrl,
      subscriptionId,
      resourceGroup,
      metricDefinition,
      resourceName,
      this.apiPreviewVersion
    );

    return this.doRequest(url).then((result: any) => {
      return ResponseParser.parseResponseValues(result, 'name', 'properties.metricNamespaceName');
    });
  }

  async getResources(subscriptionIds: string[]): Promise<Resource[]> {
    const responses: Resource[][] = await Promise.all(
      subscriptionIds.map(subscriptionId =>
        this.doRequest(`${this.baseUrl}/${subscriptionId}/resources?api-version=2018-02-01`).then(
          (res: AzureMonitorResourceResponse) =>
            res.data.value
              .map(r => ({
                ...r,
                group: /.*\/resourceGroups\/(.*?)\//.exec(r.id)[1],
                subscriptionId,
              }))
              .filter(({ type }) => this.supportedMetricNamespaces.includes(type))
        )
      )
    );

    return responses.reduce((result, resources) => [...result, ...resources], []);
  }

  getMetricNames(
    subscriptionId: string,
    resourceGroup: string,
    metricDefinition: string,
    resourceName: string,
    metricNamespace?: string
  ) {
    const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
      this.baseUrl,
      subscriptionId,
      resourceGroup,
      metricDefinition,
      resourceName,
      this.apiVersion,
      metricNamespace
    );

    return this.doRequest(url).then((result: any) => {
      return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
    });
  }

  getMetricMetadata(
    subscriptionId: string,
    resourceGroup: string,
    metricDefinition: string,
    resourceName: string,
    metricName: string,
    metricNamespace?: string
  ) {
    const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
      this.baseUrl,
      subscriptionId,
      resourceGroup,
      metricDefinition,
      resourceName,
      this.apiVersion,
      metricNamespace
    );

    return this.doRequest(url).then((result: any) => {
      return ResponseParser.parseMetadata(result, metricName);
    });
  }

  testDatasource() {
    if (!this.isValidConfigField(this.instanceSettings.jsonData.tenantId)) {
      return {
        status: 'error',
        message: 'The Tenant Id field is required.',
      };
    }

    if (!this.isValidConfigField(this.instanceSettings.jsonData.clientId)) {
      return {
        status: 'error',
        message: 'The Client Id field is required.',
      };
    }

    const url = `/${this.cloudName}/subscriptions?api-version=2019-03-01`;
    return this.doRequest(url)
      .then((response: any) => {
        if (response.status === 200) {
          return {
            status: 'success',
            message: 'Successfully queried the Azure Monitor service.',
            title: 'Success',
          };
        }

        return {
          status: 'error',
          message: 'Returned http status code ' + response.status,
        };
      })
      .catch((error: any) => {
        let message = 'Azure Monitor: ';
        message += error.statusText ? error.statusText + ': ' : '';

        if (error.data && error.data.error && error.data.error.code) {
          message += error.data.error.code + '. ' + error.data.error.message;
        } else if (error.data && error.data.error) {
          message += error.data.error;
        } else if (error.data) {
          message += error.data;
        } else {
          message += 'Cannot connect to Azure Monitor REST API.';
        }
        return {
          status: 'error',
          message: message,
        };
      });
  }

  isValidConfigField(field: string) {
    return field && field.length > 0;
  }

  doRequest(url: string, maxRetries = 1) {
    return this.backendSrv
      .datasourceRequest({
        url: this.url + url,
        method: 'GET',
      })
      .catch((error: any) => {
        if (maxRetries > 0) {
          return this.doRequest(url, maxRetries - 1);
        }

        throw error;
      });
  }
}
